JUnit Java Testing - Complete Developer Guide
Table of Contents
- Introduction to JUnit
- Core Annotations
- Assertions
- Test Lifecycle
- Parameterized Tests
- Exception Testing
- Timeout Testing
- Test Organization
- Mocking with Mockito
- Best Practices
Introduction to JUnit
JUnit is the most popular testing framework for Java applications. It provides annotations to identify test methods, assertions to verify expected results, and test runners to execute tests.
Why JUnit is Essential:
- Unit Testing: Validates individual components work correctly
- Regression Prevention: Catches bugs when code changes
- Documentation: Tests serve as living documentation
- Confidence: Enables refactoring with confidence
JUnit 5 Architecture:
- JUnit Platform: Foundation for launching testing frameworks
- JUnit Jupiter: Programming and extension model for JUnit 5
- JUnit Vintage: Backward compatibility with JUnit 3 and 4
Core Annotations
@Test - The Foundation
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class CalculatorTest {
@Test
void testAddition() {
Calculator calc = new Calculator();
int result = calc.add(2, 3);
assertEquals(5, result, "2 + 3 should equal 5");
}
}
@DisplayName - Better Test Descriptions
@Test
@DisplayName("Should calculate correct sum when adding positive numbers")
void testAddPositiveNumbers() {
// Test implementation
}
@Disabled - Skip Tests Temporarily
@Test
@Disabled("Feature not yet implemented")
void testFeatureNotReady() {
// This test will be skipped
}
Assertions
Basic Assertions (Most Frequently Used)
import static org.junit.jupiter.api.Assertions.*;
@Test
void basicAssertions() {
// Equality checks
assertEquals(expected, actual);
assertEquals(expected, actual, "Custom error message");
// Boolean checks
assertTrue(condition);
assertFalse(condition);
// Null checks
assertNull(object);
assertNotNull(object);
// Reference checks
assertSame(expected, actual); // Same object reference
assertNotSame(expected, actual);
}
Array and Collection Assertions
@Test
void collectionAssertions() {
String[] expected = {"apple", "banana", "cherry"};
String[] actual = {"apple", "banana", "cherry"};
// Array comparison
assertArrayEquals(expected, actual);
// Collection comparison
List<String> expectedList = Arrays.asList("a", "b", "c");
List<String> actualList = Arrays.asList("a", "b", "c");
assertEquals(expectedList, actualList);
}
Exception Assertions
@Test
void exceptionAssertions() {
// Assert that exception is thrown
Exception exception = assertThrows(
IllegalArgumentException.class,
() -> calculator.divide(10, 0)
);
assertEquals("Cannot divide by zero", exception.getMessage());
// Assert no exception is thrown
assertDoesNotThrow(() -> calculator.add(1, 2));
}
Advanced Assertions
@Test
void advancedAssertions() {
Person person = new Person("John", 25);
// Multiple assertions executed together
assertAll("person properties",
() -> assertEquals("John", person.getName()),
() -> assertEquals(25, person.getAge()),
() -> assertTrue(person.isAdult())
);
}
Test Lifecycle
Setup and Teardown Annotations
import org.junit.jupiter.api.*;
public class DatabaseTest {
private static Database database;
private Connection connection;
@BeforeAll
static void setupDatabase() {
// Runs once before all tests in the class
database = new Database();
database.initialize();
}
@BeforeEach
void setupConnection() {
// Runs before each test method
connection = database.getConnection();
connection.beginTransaction();
}
@Test
void testUserCreation() {
// Test implementation
}
@AfterEach
void cleanupConnection() {
// Runs after each test method
connection.rollback();
connection.close();
}
@AfterAll
static void cleanupDatabase() {
// Runs once after all tests in the class
database.shutdown();
}
}
Important Note: @BeforeAll
and @AfterAll
methods must be static.
Parameterized Tests
@ValueSource - Simple Parameter Testing
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.*;
@ParameterizedTest
@ValueSource(ints = {1, 2, 3, 5, 8, 13})
@DisplayName("Should return true for positive numbers")
void testPositiveNumbers(int number) {
assertTrue(MathUtils.isPositive(number));
}
@ParameterizedTest
@ValueSource(strings = {"", " ", " "})
void testBlankStrings(String input) {
assertTrue(StringUtils.isBlank(input));
}
@CsvSource - Multiple Parameters
@ParameterizedTest
@CsvSource({
"1, 1, 2",
"2, 3, 5",
"5, 7, 12"
})
void testAddition(int a, int b, int expected) {
assertEquals(expected, calculator.add(a, b));
}
@MethodSource - Complex Data
@ParameterizedTest
@MethodSource("providePersonData")
void testPersonValidation(String name, int age, boolean expectedValid) {
Person person = new Person(name, age);
assertEquals(expectedValid, person.isValid());
}
static Stream<Arguments> providePersonData() {
return Stream.of(
Arguments.of("John", 25, true),
Arguments.of("", 25, false),
Arguments.of("Jane", -5, false)
);
}
Exception Testing
Testing Expected Exceptions
@Test
void testDivisionByZero() {
ArithmeticException exception = assertThrows(
ArithmeticException.class,
() -> calculator.divide(10, 0),
"Division by zero should throw ArithmeticException"
);
assertTrue(exception.getMessage().contains("zero"));
}
@Test
void testValidInput() {
// Ensure no exception is thrown with valid input
assertDoesNotThrow(() -> calculator.divide(10, 2));
}
Timeout Testing
@Timeout Annotation
import org.junit.jupiter.api.Timeout;
import java.util.concurrent.TimeUnit;
@Test
@Timeout(value = 2, unit = TimeUnit.SECONDS)
void testLongRunningOperation() {
// This test will fail if it takes longer than 2 seconds
heavyComputation();
}
@Test
void testWithAssertTimeout() {
assertTimeout(Duration.ofSeconds(2), () -> {
// Code that should complete within 2 seconds
return heavyComputation();
});
}
Test Organization
Nested Test Classes
import org.junit.jupiter.api.Nested;
public class CalculatorTest {
@Nested
@DisplayName("Addition Tests")
class AdditionTests {
@Test
@DisplayName("Should add positive numbers correctly")
void addPositiveNumbers() {
assertEquals(5, calculator.add(2, 3));
}
@Test
@DisplayName("Should handle negative numbers")
void addNegativeNumbers() {
assertEquals(-1, calculator.add(-3, 2));
}
}
@Nested
@DisplayName("Division Tests")
class DivisionTests {
@Test
void divideNormalNumbers() {
assertEquals(2.5, calculator.divide(5, 2));
}
}
}
Test Ordering
import org.junit.jupiter.api.TestMethodOrder;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class OrderedTests {
@Test
@Order(1)
void firstTest() {
// Runs first
}
@Test
@Order(2)
void secondTest() {
// Runs second
}
}
Mocking with Mockito
Basic Mocking Setup
import org.mockito.*;
import static org.mockito.Mockito.*;
public class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
}
@Test
void testFindUser() {
// Arrange
User mockUser = new User("John", "john@email.com");
when(userRepository.findById(1L)).thenReturn(mockUser);
// Act
User result = userService.findUser(1L);
// Assert
assertEquals("John", result.getName());
verify(userRepository).findById(1L);
}
}
Argument Matchers
@Test
void testWithArgumentMatchers() {
// Match any string
when(userRepository.findByEmail(anyString())).thenReturn(new User());
// Match specific conditions
when(userRepository.findByAge(argThat(age -> age > 18)))
.thenReturn(Arrays.asList(new User()));
// Match exact values
when(userRepository.save(eq(user))).thenReturn(user);
}
Stubbing Void Methods
@Test
void testVoidMethod() {
// For void methods, use doNothing(), doThrow(), etc.
doNothing().when(emailService).sendEmail(anyString());
// Test the service
userService.registerUser(user);
verify(emailService).sendEmail(user.getEmail());
}